程式在執行的時候,有些時候我們會遇到一些例外的情況,我們一般會使用 try-catch
來攔截程式執行所拋出的例外,用 try-catch 攔截到之後,我們就可以視情況要自己處理還是要再把這個例外轉拋出去。(或是不處理,就讓系統崩潰)
假設有一個函式,執行之後會有可能會拋出 RuntimeException
。所以當程式執行到這個函式的時候,就必須面臨處理它的問題。
fun throwException() {
throw RuntimeException("Incorrect")
}
fun main(){
throwException()
}
→ 系統崩潰並收到一個錯誤訊息。(Exception in thread "main" java.lang.RuntimeException: Incorrect)
fun main(){
try {
throwException()
} catch (e: RuntimeException) {
println(e.message)
}
}
→ 在 try-catch
中攔截到這個例外,所以我們可以針對發生這個例外的情況來做處理。(如上方把錯誤資訊列印出來)
fun launchExceptionFun1(){
val job = launch {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("First children are cancelled")
}
}
launch {
delay(100)
println("Second child throws an exception")
throw RuntimeException()
}
}
job.join()
}
→ 這段程式使用 launch 建立了兩個子 Coroutine ,一個 launch 執行一段很長時間的延遲,另一個則是延遲 100 毫秒之後,就拋出 RuntimeException()
。
執行這段程式碼試試看:
流程如下,第二個 coroutine 拋出 RuntimeException
後,父 coroutine 的 Job 就把剩下的所有子 coroutine 取消。所以當例外發生的時候,後面還沒有執行完成的 coroutine 就會被取消。
如果我們本來就知道哪一個函式會發生例外,我們可以直接使用 try-catch,把例外自行處理掉,就不會傳到父 coroutine 來處理了。
將前面的範例改成:
launch {
delay(100)
println("Second child throws an exception")
try{
throw RuntimeException()
}catch(e: RuntimeException){
println("Catch exception")
}
}
→ 第一個 coroutine 不會因為第二個 coroutine 發生例外而被取消。
在建立 Coroutine 的時候,我們可以建立 CoroutineContext.Element 帶入,其中有一個 Element 就是用來做例外處理的。
其名稱為 CoroutineExceptionHandler
將上方程式改為:
class Day10 {
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
private val scope = CoroutineScope(coroutineExceptionHandler)
suspend fun launchWithException(){
val job = scope.launch {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
println("First children are cancelled")
}
}
launch {
delay(100)
println("Second child throws an exception")
throw RuntimeException()
}
}
job.join()
}
}
→ 我們使用 CoroutineExceptionHandler
這個方法來建立 CoroutineExceptionHandler
的實例。
這邊聽起來很饒舌,在 Coroutine 中,有一個介面名為 CoroutineExceptionHandler ,它是繼承 CoroutineContext.Element ,所以我們可以實作它並傳進 Coroutine Context 中。
另外, Coroutine 也同時提供了一個函式,用來建立這個介面的實例,而這個函式的名稱也叫做 CoroutineExceptionHandler。
這個方法實作如下:
public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineContext, Throwable) -> Unit): CoroutineExceptionHandler =
object : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
override fun handleException(context: CoroutineContext, exception: Throwable) =
handler.invoke(context, exception)
}
發生例外時就會調用 handleException,把 Coroutine context 以及 exception 傳出去。
上面的程式改寫完後,我們可以測試一下:
fun main() = runBlocking{
val day10 = Day10()
day10.launchWithException()
}
當我們的 Coroutine Scope 中有包含 CoroutineExceptionHandler 時,所有未經處理的例外都會傳到這邊,我們就可以在這個地方去做處理。
上面的範例是使用 launch
來示範的,如果是 async
,我們也可以使用 CoroutineExceptionHandler 來攔截例外嗎?
suspend fun asyncException(): Int {
val deferred1 = scope.async {
delay(100)
10
}
val deferred2 = scope.async<Int> {
delay(200)
20
throw RuntimeException("Incorrect")
}
return deferred1.await() + deferred2.await()
}
→ 我們有一個函式,在這裡面我們用 async
建立了兩個 coroutine ,在第一個 Coroutine 中,我們延遲了 100 毫秒,並且回傳整數10,而另外一個 coroutine ,我們延遲了 200 毫秒,但是在最後發生了 RuntimeException
。
→ 這個函式的結果需要將兩個 async 的結果相加傳出去。
好的,我們把這段程式碼執行看看。
fun main() = runBlocking{
val day10 = Day10()
val result = day10.asyncException()
println($result)
}
在 Coroutine 中,只有 launch
以及 actor
裏面的例外能夠被 CoroutineExceptionHandler 給攔截, async
以及 produce
的例外則是會往外拋給使用者。
在這邊我們可以使用 try-catch 來攔截,我們把上面的範例程式用 try-catch 包起來
fun main() = runBlocking{
val day10 = Day10()
try {
val result = day10.asyncException()
println("$result")
} catch (e: RuntimeException) {
println("${e.message}")
}
}
沒錯,用 try-catch 就可以把 async 的結果攔截下來了。
如果 Coroutine 的 job 為 Job(),在 Coroutine 一層一層的架構下,只要有一個 coroutine 發生例外就會導致其他的子 coroutine 被取消,如果想要避免這個情況,可以在可能發生例外的地方加上 try-catch 來作保護,讓程式不會因為例外而取消所有的 coroutine。
假如我們沒有把例外攔截下來,最後就會傳到父 coroutine 的 CoroutineExceptionHandler (如果有設定的話)。
另外,launch 與 async 處理例外的方式各有不同, launch 是會往前傳直到父 coroutine 的 CoroutineExceptionHandler,async 是把例外直接傳給呼叫的地方,故我們需要在呼叫的地方使用 try-catch 攔截。
有興趣的讀者歡迎參考:https://coroutine.kotlin.tips/
天瓏書局